/*
 * Copyright (c) 2007, Tim Stack
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Tim Stack nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY TIM STACK ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL TIM STACK BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <ctype.h>
#include <assert.h>
#include <string.h>

#include "reflecxx.h"

static const char *check_special(const char *str, int *len_out)
{
    const char *retval;

    assert(len_out != NULL);
    
    switch (*str) {
    case '\\':
	retval = "\\\\";
	*len_out = 2;
	break;
    case '\t':
	retval = "\\t";
	*len_out = 2;
	break;
    case '"':
	retval = "\\\"";
	*len_out = 2;
	break;
    case '\b':
	retval = "\\b";
	*len_out = 2;
	break;
    case '\f':
	retval = "\\f";
	*len_out = 2;
	break;
    case '\r':
	retval = "\\r";
	*len_out = 2;
	break;
    case '\n':
	retval = "\\n";
	*len_out = 2;
	break;
    default:
	retval = NULL;
	*len_out = 0;
	break;
    }

    return retval;
}

char *reflecxx_escape(const char *str)
{
    char *retval = NULL;
    size_t slen, rlen;

    if (str == NULL)
	return retval;

    slen = strlen(str);
    rlen = (size_t)((double)slen * 1.1) + 2 + 1;
    if ((retval = reflecxx_malloc(rlen)) != NULL) {
	int lpc, talen, rindex;
	const char *toappend;

	retval[0] = '"';
	for (lpc = 0, rindex = 1; str[lpc] != '\0' && retval != NULL; lpc++) {
	    if ((toappend = check_special(&str[lpc], &talen)) == NULL) {
		if (!(isalnum(str[lpc]) ||
		      ispunct(str[lpc]) ||
		      str[lpc] == ' ')) {
		    char hexdigits[16];
		    
		    snprintf(hexdigits, sizeof(hexdigits),
			     "\\u%04x",
			     str[lpc] & 0xff);
		    toappend = hexdigits;
		    talen = strlen(hexdigits);
		}
		else {
		    toappend = &str[lpc];
		    talen = 1;
		    lpc += 1;
		    while ((isalnum(str[lpc]) || str[lpc] == ' ') &&
			   str[lpc] != '\0') {
			talen += 1;
			lpc += 1;
		    }
		    lpc -= 1;
		}
	    }
	    
	    if (talen >= (int)(rlen - rindex - 2)) {
		char *old_retval = retval;
		
		rlen = (size_t)(rlen + talen + (slen - lpc) * 1.2);
		retval = reflecxx_realloc(old_retval, rlen);
		if (retval == NULL)
		    reflecxx_free(old_retval);
	    }

	    if (retval != NULL) {
		memcpy(&retval[rindex], toappend, talen);
		rindex += talen;
	    }
	}

	if (retval != NULL) {
	    retval[rindex] = '"';
	    rindex += 1;
	    retval[rindex] = '\0';
	}
    }
    
    return retval;
}

static int expect_unicode(const char *str, int *index_inout)
{
    int lpc, retval = 1;

    assert(str != NULL);
    assert(*str == 'u');
    assert(index_inout != NULL);

    str += 1;
    for (lpc = 0; lpc < 4; lpc++) {
	if (!ishexnumber(str[lpc]))
	    retval = 0;
    }

    *index_inout += 5;
    
    return retval;
}

static int expect_escape(const char *str, int *index_inout)
{
    int retval = 0;

    assert(str != NULL);
    assert(*str == '\\');
    assert(index_inout != NULL);
    
    str += 1;
    switch (*str) {
    case 'u':
	retval = expect_unicode(str, index_inout);
	break;
    case '"':
    case '\\':
    case 't':
    case 'b':
    case 'f':
    case 'r':
    case 'n':
	*index_inout += 1;
	retval = 1;
	break;
    }
    
    return retval;
}

static int unescaped_length(const char *str)
{
    int retval;

    if (str == NULL) {
	retval = -1;
    }
    else if (str[0] != '"') {
	retval = -1;
    }
    else {
	int lpc;
	
	for (retval = 0, lpc = 1;
	     str[lpc] != '"' && str[lpc] != '\0';
	     retval++, lpc++) {
	    if (str[lpc] == '\\') {
		if (!expect_escape(&str[lpc], &lpc)) {
		    retval = -1;
		    break;
		}
	    }
	}

	if (retval != -1)
	    retval += 1;
    }

    assert(retval == -1 || retval > 0);
    
    return retval;
}

char *reflecxx_unescape(const char *str, unsigned int *length_out)
{
    int lpc, slen;
    char *retval;

    *length_out = 0;
    slen = unescaped_length(str);
    
    if (slen == -1) {
	retval = NULL;
    }
    else if ((retval = reflecxx_malloc(slen)) != NULL) {
	int rindex;

	for (lpc = 1, rindex = 0;
	     str[lpc] != '"' && retval != NULL;
	     lpc++, rindex++) {
	    int ok = 1;

	    if (str[lpc] == '\\') {
		lpc += 1;
		switch (str[lpc]) {
		case '\\':
		    retval[rindex] = '\\';
		    break;
		case 't':
		    retval[rindex] = '\t';
		    break;
		case '"':
		    retval[rindex] = '"';
		    break;
		case 'b':
		    retval[rindex] = '\b';
		    break;
		case 'f':
		    retval[rindex] = '\f';
		    break;
		case 'r':
		    retval[rindex] = '\r';
		    break;
		case 'n':
		    retval[rindex] = '\n';
		    break;
		case 'u':
		    {
			int hval;
			
			if (sscanf(&str[lpc], "u%04x", &hval) != 1) {
			    ok = 0;
			}
			else {
			    retval[rindex] = hval;
			    lpc += 4;
			}
		    }
		    break;
		default:
		    ok = 0;
		    break;
		}
	    }
	    else if (isalnum(str[lpc]) ||
		     ispunct(str[lpc]) ||
		     str[lpc] == ' ') {
		retval[rindex] = str[lpc];
	    }
	    else {
		ok = 0;
	    }

	    if (!ok) {
		reflecxx_free(retval);
		retval = NULL;
	    }
	}

	if (str[lpc] != '"') {
	    reflecxx_free(retval);
	    retval = NULL;
	}
	if (retval != NULL)
	    retval[rindex] = '\0';
	*length_out = lpc + 1;
    }

    return retval;
}
